#include "prerequisites.h"
#include "render_device.h"
#include "texture.h"
#include "material.h"
#include "mesh.h"
#include "entity.h"
#include "camera.h"
#include "scene.h"

#include "vector4.h"
#include "matrix4x4.h" 

struct demo_config
{
	unsigned int num_rt_used; 
	unsigned int num_mrt; 
	D3DFORMAT texture_fmt; 

	demo_config(d3d9_device_ptr& device)
		: num_mrt(3)
	{
		BOOST_ASSERT(device);
		get_config(device); 
	}
	
	void get_config(d3d9_device_ptr& device) 
	{
		HRESULT hr; 

		// check device caps
		D3DCAPS9 caps;
		V(device->GetDeviceCaps(&caps)); 
		
		// Determine which of D3DFMT_A16B16G16R16F or D3DFMT_A8R8G8B8
		// to use for scene-rendering RTs.
		d3d9_ptr d3d9;
		device->GetDirect3D( &d3d9 );
		D3DDISPLAYMODE display_mode;
		device->GetDisplayMode( 0, &display_mode );

		if( FAILED( d3d9->CheckDeviceFormat( caps.AdapterOrdinal, caps.DeviceType,
			display_mode.Format, D3DUSAGE_RENDERTARGET,
			D3DRTYPE_TEXTURE, D3DFMT_A16B16G16R16F ) ) )
			texture_fmt = D3DFMT_A8R8G8B8;
		else
			texture_fmt = D3DFMT_A16B16G16R16F;
		
		
	}
};
typedef boost::shared_ptr<demo_config> demo_config_ptr;

// This is the vertex format used with the quad during post-process.
struct pp_quad_vert
{
	float x, y, z, rhw;
	float tu, tv;       // Texcoord for post-process source
	float tu2, tv2;     // Texcoord for the original scene

	const static D3DVERTEXELEMENT9 decl[4];
};

// Vertex declaration for post-processing
const D3DVERTEXELEMENT9 pp_quad_vert::decl[4] =
{
	{ 0, 0,  D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITIONT, 0 },
	{ 0, 16, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD,  0 },
	{ 0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD,  1 },
	D3DDECL_END()
};

class c_render_target_chain
{
public: 
	c_render_target_chain(const std::vector<d3d9_texture_ptr> rt_vec)
		: m_next(0)
		, m_is_first_rt(true)
	{
		BOOST_ASSERT(rt_vec.size() == 2); 
		m_rt_vec.reserve(2); 
		std::copy(rt_vec.begin(), rt_vec.end(), m_rt_vec.begin()); 
	}
	
	void swap()
	{
		m_next = 1 - m_next; 
	}
	
	d3d9_texture_ptr get_source() { return m_rt_vec[1 - m_next]; }
	d3d9_texture_ptr get_target() { return m_rt_vec[m_next]; }
	d3d9_texture_ptr get_prev_target() { return get_source(); }
	d3d9_texture_ptr get_prev_source() { return get_target(); }
	d3d9_texture_ptr get_next_target() { return get_source(); }
	d3d9_texture_ptr get_next_source() { return get_target(); }
	
private:
	int m_next;
	bool m_is_first_rt;
	std::vector<d3d9_texture_ptr> m_rt_vec; 
}; 
typedef boost::shared_ptr<c_render_target_chain> rt_chain_ptr;

class c_post_process_effect 
{
private:
	d3dx_effect_ptr m_effect;
	int m_render_target; 
	
}; 

demo_config_ptr g_app_config;
render_device_ptr g_render_device;

boost::shared_ptr<c_scene> g_scene_mgr = boost::shared_ptr<c_scene>(new c_scene()); 
camera_ptr g_cam;

// render targets textures
std::vector<rt_chain_ptr> g_rt_chain_vec;  
std::vector<d3d9_texture_ptr> g_scene_save_rt_vec; 
std::vector<d3d9_surface_ptr> g_scene_save_surface_vec;

d3d9_texture_ptr g_scene_save_rtt; 
d3d9_texture_ptr g_pp_rtt; 

d3d9_verdecl_ptr g_pp_quad_vert_decl;
d3dx_effect_ptr g_pp_effect;
d3dx_effect_ptr g_pp_final_effect; 

/** Text rendering */ 
d3dx_font_ptr g_d3dx_font;
d3dx_sprite_ptr g_d3dx_sprite;

bool CALLBACK ModifyDeviceSettings( DXUTDeviceSettings* pDeviceSettings, void* pUserContext );
HRESULT CALLBACK on_d3d9_device_created(IDirect3D9 *pD3dDevice, const D3DSURFACE_DESC *pBackBufferSurfaceDesc, void *pUserContext);
void CALLBACK on_d3d9_device_destroyed(void *pUserContext);
void CALLBACK on_d3d9_device_lost(void *pUserContext); 
HRESULT CALLBACK on_d3d9_device_reset(IDirect3DDevice9 *pd3dDevice, const D3DSURFACE_DESC *pBackBufferSurfaceDesc, void *pUserContext); 
void CALLBACK on_d3d9_frame_render(IDirect3DDevice9 *pd3dDevice, double time, float dt, void *pUserContext);
void CALLBACK on_frame_move(double time, float dt, void *pUserContext); 
LRESULT CALLBACK msg_prog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, bool *pbNoFurtherProcessing, void *pUserContext); 

d3dx_font_ptr create_font(); 
void destroy_font(d3dx_font_ptr& d3dx_font);
d3dx_sprite_ptr create_sprite(); 
void destroy_sprite(d3dx_sprite_ptr& d3dx_sprite); 
void setup_scene(); 
void destroy_scene(); 
void perform_post_process(); 
void render_text();

/** Definitions */ 
void setup_scene()
{	
    // textures
    texture_ptr stone_tex = create_texture_from_file("./data/materials/textures/stones.bmp");
    texture_ptr wall_tex = create_texture_from_file("./data/materials/textures/env2.bmp");
    texture_ptr wall_tex_2 = create_texture_from_file("./data/materials/textures/wall.jpg");

    // materials
    material_ptr wall_material = create_material_from_effect_file("./data/materials/scripts/default_texture.fx");
    material_ptr floor_material = create_material_from_effect_file("./data/materials/scripts/default_texture.fx"); 
    wall_material->set_diffuse_texture(0, wall_tex); 
    floor_material->set_diffuse_texture(0, stone_tex);

    // meshes
    mesh_ptr cube_mesh = make_room_mesh(5.0f, 2.0f, 5.0f);
    c_d3dx_mesh::submesh_iterator it = cube_mesh->submesh_begin();
    (*it++)->set_material(wall_material);
    (*it++)->set_material(floor_material); 
    (*it++)->set_material(floor_material); 
    
    mesh_entity_ptr cube_ent = make_entity(std::string("cube"), cube_mesh);
    g_scene_mgr->add_scene_node(cube_ent); 

	const D3DSURFACE_DESC* pBackBufferDesc = DXUTGetD3D9BackBufferSurfaceDesc();

    // create camera
    c_vector3 cam_pos(2, 1, 1);
    c_vector3 cam_lookat(2, 1, 2.0f);
    c_vector3 cam_up(0, 1.0f, 0); 
    g_cam = g_scene_mgr->create_camera(
		"main_camera", 
		cam_pos, 
		cam_lookat, 
		cam_up, 
		frustum_ptr(new c_frustum(D3DX_PI/4,(float)(pBackBufferDesc->Width)/(float)(pBackBufferDesc->Height), 0.1f, 100.0f)));
    g_cam->set_active(true); 

	// Set options for the first-person camera
	g_cam->SetScalers( 0.01f, 15.0f );
	g_cam->SetDrag( true );
	g_cam->SetEnableYAxisMovement( false );

	D3DXVECTOR3 vMin = D3DXVECTOR3( 1.5f, 0.0f, 1.5f );
	D3DXVECTOR3 vMax = D3DXVECTOR3( 13.5f, 10.0f, 18.5f );
	g_cam->SetClipToBoundary( TRUE, &vMin, &vMax );
}

void destroy_scene()
{
    g_scene_mgr.reset(); 
}

d3dx_font_ptr create_font()
{
	HRESULT hr;
	d3d9_device_ptr device = g_render_device->get_d3d_device_ptr(); 
	d3dx_font_ptr font;
	
	// Initialize the font, release in on_d3d9_device_destroyed()
	V( D3DXCreateFont( device, 15, 0, FW_BOLD, 1, FALSE, DEFAULT_CHARSET,
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
		L"Arial", &font) );
	
	return font; 
}

void destroy_font(d3dx_font_ptr& d3dx_font)
{
	d3dx_font = NULL; 
}

d3dx_sprite_ptr create_sprite()
{
	HRESULT hr; 
	d3d9_device_ptr device = g_render_device->get_d3d_device_ptr(); 
	d3dx_sprite_ptr sprite; 

	// Create a sprite to help batch calls when drawing many lines of text
	V(D3DXCreateSprite(device, &sprite)); 
	
	return sprite; 
}

void destroy_sprite(d3dx_sprite_ptr& d3dx_sprite)
{
	d3dx_sprite = NULL; 
}

bool CALLBACK ModifyDeviceSettings( DXUTDeviceSettings* pDeviceSettings, void* pUserContext )
{
    pDeviceSettings->d3d9.BehaviorFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
    return true; 
}

HRESULT CALLBACK on_d3d9_device_created( IDirect3DDevice9* pd3dDevice, const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
{
    HRESULT hr;

    g_render_device = render_device_ptr(new c_render_device9(pd3dDevice));

	// config the demo
	g_app_config.reset(new demo_config(g_render_device->get_d3d_device_ptr())); 
	
	g_d3dx_font = create_font(); 
	
    d3d9_device_ptr device = g_render_device->get_d3d_device_ptr();
    V(D3DXCreateEffectFromFileA(
        device,
        "./data/materials/scripts/pp_gblur_h.fx",
        NULL,
        NULL,
        D3DXSHADER_DEBUG,
        NULL,
        &g_pp_effect,
        NULL));
    
    V(D3DXCreateEffectFromFileA(
        device,
        "./data/materials/scripts/pp_final.fx",
        NULL,
        NULL,
        D3DXSHADER_DEBUG,
        NULL,
        &g_pp_final_effect,
        NULL));
    
    V(g_render_device->get_d3d_device_ptr()->CreateVertexDeclaration(pp_quad_vert::decl, &g_pp_quad_vert_decl));

	// Setup the scene
    setup_scene(); 

    return S_OK;
}

void CALLBACK on_d3d9_device_destroyed( void* pUserContext )
{
    destroy_scene();
	destroy_font(g_d3dx_font); 
    
    // release rtt
    g_scene_save_rtt = NULL;
    g_pp_rtt = NULL;
    
    // release effects
    g_pp_effect = NULL;
    g_pp_final_effect = NULL; 
    g_pp_quad_vert_decl = NULL; 
}

HRESULT CALLBACK on_d3d9_device_reset(IDirect3DDevice9 *pd3dDevice, const D3DSURFACE_DESC *pBackBufferSurfaceDesc, void *pUserContext)
{
	HRESULT hr;

	if (g_d3dx_font)
		g_d3dx_font->OnResetDevice(); 

	g_d3dx_sprite = create_sprite(); 

	// Create render target texture for saving the scene
	V(g_render_device->get_d3d_device_ptr()->CreateTexture( 
		pBackBufferSurfaceDesc->Width,
		pBackBufferSurfaceDesc->Height,
		1, 
		D3DUSAGE_RENDERTARGET,
		g_app_config->texture_fmt,
		D3DPOOL_DEFAULT,
		&g_scene_save_rtt, 
		NULL));
    
    // Create render target texture for pp output
    V(g_render_device->get_d3d_device_ptr()->CreateTexture( 
        pBackBufferSurfaceDesc->Width,
        pBackBufferSurfaceDesc->Height,
        1,
        D3DUSAGE_RENDERTARGET,
        D3DFMT_A8R8G8B8,
        D3DPOOL_DEFAULT,
        &g_pp_rtt, 
        NULL));

    D3DXHANDLE hConvertSource = g_pp_effect->GetParameterByName( NULL, "pixel_kernel" );
    D3DXHANDLE hParamToConvert = g_pp_effect->GetParameterByName(NULL, "texel_kernel" );
    D3DXPARAMETER_DESC desc;
    g_pp_effect->GetParameterDesc(hConvertSource, &desc );
    // Each element has 2 floats
    DWORD cKernel = desc.Bytes / ( 2 * sizeof( float ) );
    D3DXVECTOR4* pvKernel = new D3DXVECTOR4[cKernel];
    if( !pvKernel )
        return E_OUTOFMEMORY;
    g_pp_effect->GetVectorArray( hConvertSource, pvKernel, cKernel );
    // Convert
    for( DWORD i = 0; i < cKernel; ++i )
    {
        pvKernel[i].x = pvKernel[i].x / (pBackBufferSurfaceDesc->Width);
        pvKernel[i].y = pvKernel[i].y / (pBackBufferSurfaceDesc->Height);
    }
    // Copy back
    g_pp_effect->SetVectorArray( hParamToConvert, pvKernel, cKernel );

    delete[] pvKernel;

    return S_OK; 
}

void CALLBACK on_d3d9_device_lost(void *pUserContext)
{
	if (g_d3dx_font)
		g_d3dx_font->OnLostDevice();

	/*
	g_rt_chain_vec.clear();
	g_scene_save_rt_vec.clear(); 
	g_scene_save_surface_vec.clear(); 
	*/ 
    
    g_scene_save_rtt = NULL; 
    g_pp_rtt = NULL;

	destroy_sprite(g_d3dx_sprite); 
}

void CALLBACK on_d3d9_frame_render(IDirect3DDevice9 *pd3dDevice, double time, float dt, void *pUserContext)
{
    HRESULT hr; 
    d3d9_surface_ptr old_render_target; 
    g_render_device->get_d3d_device_ptr()->GetRenderTarget(0, &old_render_target);
    
    V( g_render_device->get_d3d_device_ptr()->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DXCOLOR( 0.0f, 0.25f, 0.25f, 0.55f ), 1.0f,
        0 ) );

    d3d9_surface_ptr surface;
    g_scene_save_rtt->GetSurfaceLevel(0, &surface); 
    V(g_render_device->get_d3d_device_ptr()->SetRenderTarget(0, surface)); 
    
    // Render scene onto a texture
    if (SUCCEEDED(g_render_device->get_d3d_device_ptr()->BeginScene()))
    {
        g_scene_mgr->render_scene(); 
        V( g_render_device->get_d3d_device_ptr()->EndScene()); 
    }

    // Perform post-process on the render target texture
    perform_post_process();
   
    // restore old render target (back buffer)
    V(g_render_device->get_d3d_device_ptr()->SetRenderTarget(0, old_render_target));
    
    // Get the final result image onto the backbuffer
    const D3DSURFACE_DESC* pd3dsdBackBuffer = DXUTGetD3D9BackBufferSurfaceDesc();
    if (SUCCEEDED(g_render_device->get_d3d_device_ptr()->BeginScene()))
    {
        // Render a screen-sized quad
        pp_quad_vert quad[4] =
        {
            { -0.5f, -0.5f, 0.5f, 1.0f, 
            0.0f, 0.0f, 
            0.0f, 0.0f },

            { pd3dsdBackBuffer->Width - 0.5f, -0.5f, 0.5f, 1.0f, 
            1.0f, 0.0f, 
            0.0f, 0.0f },

            { -0.5f, pd3dsdBackBuffer->Height - 0.5f, 0.5f, 1.0f, 
            0.0f, 1.0f, 
            0.0f, 0.0f },
            
            { pd3dsdBackBuffer->Width - 0.5f, pd3dsdBackBuffer->Height - 0.5f, 0.5f, 1.0f,
            1.0f, 1.0f, 
            0.0f, 0.0f }
        };
        
        V(g_render_device->get_d3d_device_ptr()->SetVertexDeclaration(g_pp_quad_vert_decl));
        V(g_pp_final_effect->SetTechnique("pp_final_tech"));
        
        unsigned int num_passes = 0; 
        V(g_pp_final_effect->Begin(&num_passes, 0));

        V(g_pp_final_effect->SetTexture("g_pp_render_tex", g_pp_rtt));

        for (unsigned int i = 0; i < num_passes; ++i)
        {
            V(g_pp_final_effect->BeginPass(i));
            V(g_render_device->get_d3d_device_ptr()->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(pp_quad_vert)));
            V(g_pp_final_effect->EndPass());
        }
        V(g_pp_final_effect->End());
        
        // draw the text 
        render_text(); 

        V( g_render_device->get_d3d_device_ptr()->EndScene()); 
    }
}

void perform_post_process()
{
    HRESULT hr; 

    const D3DSURFACE_DESC* pd3dsdBackBuffer = DXUTGetD3D9BackBufferSurfaceDesc();
    pp_quad_vert quad[4] =
    {
        { -0.5f, -0.5f, 1.0f, 1.0f, 
        0.0f, 0.0f, 
        0.0f, 0.0f },
        
        { pd3dsdBackBuffer->Width-0.5f, -0.5, 1.0f, 1.0f, 
        1.0f, 0.0f, 
        1.0f, 0.0f },
       
        { -0.5, pd3dsdBackBuffer->Height-0.5f, 1.0f, 1.0f, 
        0.0f, 1.0f, 
        0.0f, 1.0f },

        { pd3dsdBackBuffer->Width-0.5f, pd3dsdBackBuffer->Height-0.5f, 1.0f, 1.0f, 
        1.0f, 1.0f, 
        1.0f, 1.0f }
    };
    // Create a vertex buffer for the quad and fill it with data 
    void *p_data = NULL; 
    d3d9_vb_ptr vb; 
    V(g_render_device->get_d3d_device_ptr()->CreateVertexBuffer(
        sizeof(pp_quad_vert)*4, 
        D3DUSAGE_WRITEONLY | D3DUSAGE_DYNAMIC,
        0,
        D3DPOOL_DEFAULT, 
        &vb,
        NULL));
    
    if (SUCCEEDED(vb->Lock(0, 0, &p_data, D3DLOCK_DISCARD)))
    {
        ::memcpy_s(p_data, sizeof(quad), quad, sizeof(quad));

        V(vb->Unlock()); 
    }
    
    // Set the render target
    d3d9_surface_ptr pp_rt_surface;
    V(g_pp_rtt->GetSurfaceLevel(0, &pp_rt_surface));
    V(g_render_device->get_d3d_device_ptr()->SetRenderTarget(0, pp_rt_surface)); 
    
    // Render the quad
    if (SUCCEEDED(g_render_device->get_d3d_device_ptr()->BeginScene()))
    {
        // Set the technique
        V(g_pp_effect->SetTechnique("post_process_tech"));
        
        // Set the vertex declaration
        V(g_render_device->get_d3d_device_ptr()->SetVertexDeclaration(g_pp_quad_vert_decl)); 

        //
        // Clear the render target
        //
        V(g_render_device->get_d3d_device_ptr()->Clear( 0L, NULL, D3DCLEAR_TARGET,
            0x00000000, 1.0f, 0L ));

        // Render the quad
        unsigned int num_passes = 0; 
        V(g_pp_effect->Begin(&num_passes, 0)); 

        // Setup the source texture
        D3DXHANDLE src_tex = g_pp_effect->GetParameterByName(NULL, "g_src_color_tex");
        g_pp_effect->SetTexture(src_tex, g_scene_save_rtt); 

        for (unsigned int i = 0; i < num_passes; ++i)
        {
            V(g_pp_effect->BeginPass(i));
            
            V(g_render_device->get_d3d_device_ptr()->SetStreamSource(0, vb, 0, sizeof(pp_quad_vert))); 
            V(g_render_device->get_d3d_device_ptr()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2));
            
            V(g_pp_effect->EndPass());
        }
        V(g_pp_effect->End());

        g_render_device->get_d3d_device_ptr()->EndScene(); 
    }
}

void render_text()
{
	CDXUTTextHelper text_helper(g_d3dx_font, g_d3dx_sprite, 15); 
	
	// Draw statistics text 
	text_helper.Begin();
	text_helper.SetInsertionPos( 5, 5 );
	text_helper.SetForegroundColor( D3DXCOLOR( 1.0f, 1.0f, 0.0f, 1.0f ) );
	text_helper.DrawTextLine( DXUTGetFrameStats( DXUTIsVsyncEnabled() ) );
	text_helper.DrawTextLine( DXUTGetDeviceStats() );
	text_helper.SetForegroundColor( D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f ) );
	text_helper.End();
}

void CALLBACK on_frame_move(double time, float dt, void *pUserContext)
{
	if (g_cam)
		g_cam->FrameMove(dt);

    g_scene_mgr->update_scene(dt);
}

LRESULT CALLBACK msg_prog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, bool *pbNoFurtherProcessing, void *pUserContext)
{
	// Pass all remaining windows messages to camera so it can respond to user input
	if (g_cam)
		g_cam->HandleMessages( hWnd, uMsg, wParam, lParam );

    return 0; 
}

INT WINAPI wWinMain( HINSTANCE, HINSTANCE, LPWSTR, int )
{
    // DXUTSetCallbackDeviceChanging( ModifyDeviceSettings );
    DXUTSetCallbackD3D9DeviceCreated(on_d3d9_device_created); 
    DXUTSetCallbackD3D9DeviceDestroyed(on_d3d9_device_destroyed); 
    DXUTSetCallbackD3D9DeviceLost(on_d3d9_device_lost);
    DXUTSetCallbackD3D9DeviceReset(on_d3d9_device_reset); 
    DXUTSetCallbackD3D9FrameRender(on_d3d9_frame_render);

    DXUTSetCallbackMsgProc(msg_prog);
    DXUTSetCallbackFrameMove(on_frame_move); 

    // Initialize DXUT and create the desired Win32 window and Direct3D device for the application
	WCHAR *params = L"-forcevsync:0"; 
    DXUTInit( true, true, params); // Parse the command line and show msgboxes
    DXUTSetHotkeyHandling( true, true, true );  // handle the default hotkeys
    DXUTSetCursorSettings( true, true ); // Show the cursor and clip it when in full screen
    DXUTCreateWindow( L"scene manager demo" );
    DXUTCreateDevice(true, 1024, 768);
   
    // Start the render loop
    DXUTMainLoop();
    
    return DXUTGetExitCode(); 
}